<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>编写良好的前端组件 · ShaofeiZi Blog · 做个日常记录</title>
    <meta name="description" content="编写良好的前端组件">
    <link rel="shortcut icon" href="/BLOG/favicon.ico">
  <link rel="manifest" href="/BLOG/manifest.json">
  <meta name="theme-color" content="#3F51B5">
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
  <link rel="apple-touch-icon" href="/BLOG/icons/192.png">
  <link rel="mask-icon" href="/BLOG/icons/safari-pinned-tab.svg" color="#3eaf7c">
  <meta name="msapplication-TileImage" content="/icons/192.png">
  <meta name="msapplication-TileColor" content="#3F51B5">
    
    <link rel="preload" href="/BLOG/assets/css/42.styles.90045bd1.css" as="style"><link rel="preload" href="/BLOG/assets/js/app.1a725be8.js" as="script"><link rel="preload" href="/BLOG/assets/js/1.39b9c99c.js" as="script"><link rel="prefetch" href="/BLOG/assets/js/7.88ba0bb7.js"><link rel="prefetch" href="/BLOG/assets/js/0.d3e592bd.js"><link rel="prefetch" href="/BLOG/assets/js/2.68dc10c9.js"><link rel="prefetch" href="/BLOG/assets/js/3.dfebdd5e.js"><link rel="prefetch" href="/BLOG/assets/js/4.ea97a821.js"><link rel="prefetch" href="/BLOG/assets/js/5.d8c2ecbf.js"><link rel="prefetch" href="/BLOG/assets/js/6.e51cd79c.js"><link rel="prefetch" href="/BLOG/assets/js/8.d9eebc06.js"><link rel="prefetch" href="/BLOG/assets/js/9.1a541d13.js"><link rel="prefetch" href="/BLOG/assets/js/10.4ec9ca67.js"><link rel="prefetch" href="/BLOG/assets/js/11.02558377.js"><link rel="prefetch" href="/BLOG/assets/js/12.d0e2086f.js"><link rel="prefetch" href="/BLOG/assets/js/13.5af02ddd.js"><link rel="prefetch" href="/BLOG/assets/js/14.5d9fcbf2.js"><link rel="prefetch" href="/BLOG/assets/js/15.ca0178b2.js"><link rel="prefetch" href="/BLOG/assets/js/16.cd99d056.js"><link rel="prefetch" href="/BLOG/assets/js/17.56f11c1d.js"><link rel="prefetch" href="/BLOG/assets/js/18.21837cc7.js"><link rel="prefetch" href="/BLOG/assets/js/19.73335fea.js"><link rel="prefetch" href="/BLOG/assets/js/20.1632ab79.js"><link rel="prefetch" href="/BLOG/assets/js/21.43175244.js"><link rel="prefetch" href="/BLOG/assets/js/22.5b7c0cca.js"><link rel="prefetch" href="/BLOG/assets/js/23.e624ba97.js"><link rel="prefetch" href="/BLOG/assets/js/24.ac5f7b41.js"><link rel="prefetch" href="/BLOG/assets/js/25.6934a11d.js"><link rel="prefetch" href="/BLOG/assets/js/26.407b2583.js"><link rel="prefetch" href="/BLOG/assets/js/27.7449d673.js"><link rel="prefetch" href="/BLOG/assets/js/28.52e25437.js"><link rel="prefetch" href="/BLOG/assets/js/29.fba21f3a.js"><link rel="prefetch" href="/BLOG/assets/js/30.2cd6d3e2.js"><link rel="prefetch" href="/BLOG/assets/js/31.0b0a749f.js"><link rel="prefetch" href="/BLOG/assets/js/32.92134487.js"><link rel="prefetch" href="/BLOG/assets/js/33.ad2b89cc.js"><link rel="prefetch" href="/BLOG/assets/js/34.9b22334e.js"><link rel="prefetch" href="/BLOG/assets/js/35.825f3d75.js"><link rel="prefetch" href="/BLOG/assets/js/36.cc3da84c.js"><link rel="prefetch" href="/BLOG/assets/js/37.8f339f62.js"><link rel="prefetch" href="/BLOG/assets/js/38.5674618f.js"><link rel="prefetch" href="/BLOG/assets/js/39.180f0d85.js"><link rel="prefetch" href="/BLOG/assets/js/40.275f26e3.js"><link rel="prefetch" href="/BLOG/assets/js/41.ce0f5927.js">
    <link rel="stylesheet" href="/BLOG/assets/css/42.styles.90045bd1.css">
  </head>
  <body>
    <div id="app" data-server-rendered="true"><div data-app="true" id="app" class="application theme--light"><div class="application--wrap"><div class="v-progress-linear blog-progress" style="height:3px;display:none;"><div class="v-progress-linear__background accent" style="height:3px;opacity:0.4;width:100%;"></div><div class="v-progress-linear__bar"><!----><div class="v-progress-linear__bar__determinate accent" style="width:0%;"></div></div></div><aside class="v-navigation-drawer v-navigation-drawer--close v-navigation-drawer--fixed v-navigation-drawer--is-mobile" style="height:100%;margin-top:0px;max-height:calc(100% - 0px);transform:translateX(-240px);width:240px;"><div><div class="aside-brand-wrap"><div class="aside-brand"><a href="/BLOG/" class="aside-avatar elevation-2 router-link-active"><img src="/BLOG/face.png" alt="avatar"></a><hgroup class="mt-3 variant-hide"><div class="subheading white--text">訾绍飞</div><a href="mailto:zishaofei221@gmail.com" title="zishaofei221@gmail.com" class="aside-mail primary--text text--lighten-5">zishaofei221@gmail.com</a></hgroup></div></div><hr class="v-divider theme--dark"><div class="v-list nav-list"><div class="secondary--text"><a href="/BLOG/" class="v-list__tile v-list__tile--link"><div class="v-list__tile__avatar"><div class="v-avatar" style="height:40px;width:40px;"><i class="fa fa-home"></i></div></div><div class="v-list__tile__content">首页</div></a></div><div class="secondary--text"><a href="/BLOG/tags" class="v-list__tile v-list__tile--link"><div class="v-list__tile__avatar"><div class="v-avatar" style="height:40px;width:40px;"><i class="fa fa-tag"></i></div></div><div class="v-list__tile__content">标签</div></a></div><div class="secondary--text"><a href="https://github.com/ShaofeiZi" target="_blank" class="v-list__tile v-list__tile--link"><div class="v-list__tile__avatar"><div class="v-avatar" style="height:40px;width:40px;"><i class="fab fa-github"></i></div></div><div class="v-list__tile__content">Github</div></a></div><div class="secondary--text"><a href="/BLOG/about" class="v-list__tile v-list__tile--link"><div class="v-list__tile__avatar"><div class="v-avatar" style="height:40px;width:40px;"><i class="fa fa-user-secret"></i></div></div><div class="v-list__tile__content">About</div></a></div></div></div><div class="v-navigation-drawer__border"></div></aside><nav class="blog-toolbar v-toolbar v-toolbar--fixed theme--dark primary" style="margin-top:0px;padding-right:0px;padding-left:0px;transform:translateY(0px);"><div class="v-toolbar__content" style="height:56px;"><button type="button" class="v-btn v-btn--icon"><div class="v-btn__content"><i class="fa fa-bars"></i></div></button><div class="v-toolbar__title">编写良好的前端组件</div><div class="spacer"></div><div class="search-box"><input aria-label="Search" autocomplete="off" spellcheck="false" value=""><!----></div><div class="v-menu" style="display:none;"><div class="v-menu__activator"><button type="button" class="v-btn v-btn--icon"><div class="v-btn__content"><i class="fa fa-share-alt"></i></div></button></div><div class="v-menu__content" style="max-height:auto;min-width:0px;max-width:auto;top:12px;left:0px;transform-origin:top right;z-index:0;display:none;"><div class="v-list"><div class="secondary--text"><a class="v-list__tile v-list__tile--link"><div class="v-list__tile__avatar"><div class="v-avatar" style="height:40px;width:40px;"><i class="fa fa-lg fa-copy"></i></div></div><div class="v-list__tile__title">复制链接</div></a></div></div><input type="text" tabindex="-1" aria-hidden="true" value="" class="fake-hide"></div></div></div></nav><main class="v-content" style="padding-top:56px;padding-right:0px;padding-bottom:0px;padding-left:0px;"><div class="v-content__wrap"><div class="container blog-container grid-list-xl align-center"><div class="layout row wrap"><div class="flex mb-3 xs12"><article class="v-card elevation-16 post-card" style="height:undefined;"><div class="v-card__title"><div class="flex xs12"><h2 class="display-1 mb-3">编写良好的前端组件</h2><div class="post-meta"><time datetime="2017-03-17T10:22:38.000Z" class="secondary--text post-time">2017年03月17日</time></div></div></div><div class="v-card__text pt-0 pb-0"><div class="flex xs12"><div class="content custom"><p>Vue 和 React 的大红大火，带来的是组件化和数据驱动的开发方式。Demo 很美好，但如果没有一定的实际开发经验积累，总是能把一个功能模块写成浆糊。
依托于 Webpack 等构建工具，使得前端代码具备了后端编程语言的代码组织能力，摆脱了传统的「一泻而下」式的代码编写。至此，作为前端也该对自己的代码有更高的要求。
</p><h2 id="组件职责划分"><a href="#组件职责划分" aria-hidden="true" class="header-anchor">#</a> 组件职责划分</h2><blockquote><p>一个组件只做一件事，基于功能做好职责划分。</p></blockquote><h3 id="无状态组件"><a href="#无状态组件" aria-hidden="true" class="header-anchor">#</a> 无状态组件</h3><p>公司用的是 Vue，最近又接触了下 React。
对比来说，React 由于 jsx 式（js和html混合）的写法，加上构建工具的模块化管理，一个文件中可以有多个组件。还支持纯函数式的<strong>无状态组件</strong>，只是单纯的接受数据渲染 DOM，没有生命周期等额外的概念。</p><p><img src="http://static.imys.net/no-status-component.jpg" alt="无状态组件"></p><pre class="language-js"><code><span class="token comment">// 无状态组件</span>
<span class="token keyword">const</span> <span class="token function-variable function">noStatus</span> <span class="token operator">=</span> props <span class="token operator">=&gt;</span> <span class="token operator">&lt;</span>h1<span class="token operator">&gt;</span><span class="token punctuation">{</span>props<span class="token punctuation">.</span>title<span class="token punctuation">}</span><span class="token operator">&lt;</span><span class="token operator">/</span>h1<span class="token operator">&gt;</span>
</code></pre><p>看起来就像一个简单的模版渲染过程。</p><p>Vue 中没有<strong>无状态组件</strong>的概念，但实际上也存在类似功能的组件形式。比如图标组件，只接收 <code>props</code> 渲染模版，不做多余的动作。</p><pre class="language-html"><code><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>template</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>i</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">&quot;</span>icon<span class="token punctuation">&quot;</span></span> <span class="token attr-name">:class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">&quot;</span><span class="token punctuation">'</span>icon-<span class="token punctuation">'</span> + name<span class="token punctuation">&quot;</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>i</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>template</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token punctuation">&gt;</span></span><span class="token script language-javascript">
<span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token punctuation">{</span>
    props<span class="token punctuation">:</span> <span class="token punctuation">{</span>
        name<span class="token punctuation">:</span> String
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">&gt;</span></span>
</code></pre><h3 id="端对端组件"><a href="#端对端组件" aria-hidden="true" class="header-anchor">#</a> 端对端组件</h3><p>端对端组件指的是不需要依赖外部给予，自身就可以负责从数据获取到展示过程的组件。
这类组件在业务开发中也很常见，比如公共的分类选择器。由于到多处调用，如果每次用的时候都由外部请求数据在调用组件展示，那么这个请求数据的代码显然是个重复的逻辑，索性直接就写入到组件内部了。</p><p><img src="http://static.imys.net/end-to-end-component.jpg" alt="端对端组件"></p><blockquote><p>当然端对端组件也有缺陷。就是每次调用不管数据有没有变化，都会重新请求，造成冗余。如何改善，那又是另一个话题了。这篇文章中有提到：<a href="https://github.com/xufei/blog/issues" target="_blank" rel="noopener noreferrer">徐飞：复杂单页应用的数据层设计</a></p></blockquote><h3 id="ui组件"><a href="#ui组件" aria-hidden="true" class="header-anchor">#</a> UI组件</h3><p>UI 组件指的是界面扩展类组件，比如：输入框、表格、树、下拉框等。像 Element、Vux 等组件库均属于此类组件。</p><p><img src="http://static.imys.net/ui-component.jpg" alt="UI组件"></p><p>此类组件的特点是：复用性强，只通过 <code>props</code>、<code>events</code> 和 <code>slots</code> 等组件接口与外部通信。
更像是一个对 HTML 的扩展标签。</p><h3 id="业务组件"><a href="#业务组件" aria-hidden="true" class="header-anchor">#</a> 业务组件</h3><p>业务组件通常是根据最小业务状态抽象而出，有些业务组件也具有一定的复用性，但大多数是一次性组件。</p><p><img src="http://static.imys.net/service-component.jpg" alt="业务组件"></p><p>之前提到的组件数据或自给自足（端对端组件），或来自 <code>props</code>，那么业务组件的数据呢？</p><ol><li>props</li><li>global state</li></ol><p>只能是以上两种了，如果还是组件内部去请求数据，那么就还是属于端对端组件了。</p><h3 id="容器组件"><a href="#容器组件" aria-hidden="true" class="header-anchor">#</a> 容器组件</h3><p>这类组件就是一个盒子，一般当作一个业务子模块的入口，比如一个路由指向的组件。</p><p><img src="http://static.imys.net/container-component.jpg" alt="容器组件"></p><p>通常是这种形式：</p><pre class="language-html"><code><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>moduleA</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>moduleA</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>moduleB</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>moduleB</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>moduleC</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>moduleC</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
</code></pre><ul><li>容器组件内的子组件通常具有业务或数据依赖关系。</li><li>如果没有使用全局状态管理，那么容器组件就是负责通过 <code>props</code> 分发数据到各个子组件，在通过 <code>events</code> 处理各个子组件的业务响应。此时容器组件需要做数据请求工作。</li><li>如果使用了全局状态管理，那么容器内部的业务组件可以自行调用全局状态处理业务。但并不是说此时容器组件什么都不用干了。即使不需要请求数据，还是有许多组件间或一个业务模块内的诸多统筹工作要做。</li></ul><p>把上面的各类组件组装到一起就组成一个业务模块。</p><p><img src="http://static.imys.net/module-and-components.jpg" alt="业务模块"></p><h2 id="组件设计原则"><a href="#组件设计原则" aria-hidden="true" class="header-anchor">#</a> 组件设计原则</h2><h3 id="尽可能的减少状态"><a href="#尽可能的减少状态" aria-hidden="true" class="header-anchor">#</a> 尽可能的减少状态</h3><ol><li>如果一个数据可以由另一个 state 变换得到，那么这个数据就不是一个 state。只需要写一个变换的处理函数，在 Vue 中可以使用计算属性。</li><li>如果你的 state 是一个数组，而模版最外层是渲染这个数组，那么你需要做的事是把渲染的项作为一个组件，只接受一个单级对象形式的数据，由外部决定这个组件的展示次数。</li><li>如果一个数据是固定的，不会变化的常量，那么这个数据就如同 HTML 固定的站点标题一样，写死或作为全局配置属性等，不属于 state。</li><li>如果一个数据需要从外部得到，它应该属于 props。</li><li>如果组件和兄弟组件拥有相同的 state，那么这个 state 应该放到更高的层级中，使用 props 传递到两个组件中。</li></ol><h3 id="合理的依赖关系"><a href="#合理的依赖关系" aria-hidden="true" class="header-anchor">#</a> 合理的依赖关系</h3><ol><li>父组件不依赖子组件。要做到当我们把子组件删除后，只是丢失了一个功能，或一个模块等，而不会造成父组件及兄弟组件功能异常。</li><li>子组件基于父组件传递 props 作出个性化展示。</li></ol><h3 id="扁平化参数"><a href="#扁平化参数" aria-hidden="true" class="header-anchor">#</a> 扁平化参数</h3><p>像 HTML 原生元素那样，只接受原始类型（字符串、数值、布尔值和函数）作为属性，避免复杂的对象。当然，数据除外。</p><pre class="language-html"><code><span class="token comment">&lt;!-- good --&gt;</span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>my-component</span>
  <span class="token attr-name">label</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">&quot;</span>hello<span class="token punctuation">&quot;</span></span>
  <span class="token attr-name">:actived</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">&quot;</span>true<span class="token punctuation">&quot;</span></span>
  <span class="token attr-name">:width</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">&quot;</span>600<span class="token punctuation">&quot;</span></span>
  <span class="token attr-name">:on-show</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">&quot;</span>show<span class="token punctuation">&quot;</span></span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>my-component</span><span class="token punctuation">&gt;</span></span>

<span class="token comment">&lt;!-- bad --&gt;</span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>my-component</span> <span class="token attr-name">:config</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">&quot;</span>myConfig<span class="token punctuation">&quot;</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>my-component</span><span class="token punctuation">&gt;</span></span>
</code></pre><h3 id="良好的接口设计"><a href="#良好的接口设计" aria-hidden="true" class="header-anchor">#</a> 良好的接口设计</h3><ol><li>把组件内部可以完成的工作做到极致。虽然提倡拥抱变化，但接口不是越多越好。</li><li>如果常量变为 props 能应对更多的场景，那么就可以作为 props。原有的常量可作为默认值。</li><li>如果组件不能提供调用者所需求的功能，那么这个组件的接口还不够完善。</li><li>如果需要为了某一调用者编写大量特定需求的代码，那么可以考虑通过扩展等方式构建一个新的组件。</li><li>保证组件的属性和事件足够的给大多数的组件使用。</li></ol><h2 id="end"><a href="#end" aria-hidden="true" class="header-anchor">#</a> End</h2><p>设计模式六大原则在组件设计中也有适用的地方。</p></div></div></div><div class="v-card__actions"><div class="flex xs12"><a href="/BLOG/tags/前端"><span tabindex="0" class="v-chip capitalize chip-tag v-chip--label v-chip--small"><span class="v-chip__content">前端</span></span></a><a href="/BLOG/tags/组件"><span tabindex="0" class="v-chip capitalize chip-tag v-chip--label v-chip--small"><span class="v-chip__content">组件</span></span></a><a href="/BLOG/tags/Vue"><span tabindex="0" class="v-chip capitalize chip-tag v-chip--label v-chip--small"><span class="v-chip__content">Vue</span></span></a><a href="/BLOG/tags/React"><span tabindex="0" class="v-chip capitalize chip-tag v-chip--label v-chip--small"><span class="v-chip__content">React</span></span></a></div></div></article></div><div class="flex text-xs-left xs6"><a href="/BLOG/posts/vue-best-practices.html" class="post-nav v-btn v-btn--flat v-btn--router"><div class="v-btn__content"><div class="grey--text"><i class="fa mr-1 fa-chevron-left"></i>Prev</div><div class="title mt-1 primary--text hidden-xs-only">Vue最佳实践</div></div></a></div><div class="flex text-xs-right xs6"><a href="/BLOG/posts/webpack-use-lodash.html" class="post-nav v-btn v-btn--flat v-btn--router"><div class="v-btn__content"><div class="grey--text">Next
          <i class="fa ml-1 fa-chevron-right"></i></div><div class="title mt-1 primary--text hidden-xs-only">Webpack按需打包Lodash的几种方式</div></div></a></div><div class="flex mt-3 xs12"><div class="v-card" style="height:undefined;"><div class="v-card__title"><span class="headline">Comment</span></div></div></div></div></div><footer class="v-footer blog-footer darken-1 mt-3 theme--dark" style="height:auto;"><div class="primary--text text--lighten-4 text-xs-center py-3 v-card v-card--flat v-card--tile primary" style="height:undefined;"><div class="v-card__text pb-0">博客内容遵循 <a rel="license noopener noreferrer" href="https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh" target="_blank">知识共享 署名 - 非商业性 - 相同方式共享 4.0 国际协议</a></div><div class="v-card__text pt-0 mt-1"><span>訾绍飞 © 2015 - 2018</span><span><!---->
        Power by
        <a href="https://vuepress.vuejs.org" target="_blank" rel="noopener noreferrer">VuePress</a> Theme
        <a href="https://github.com/ShaofeiZi/BLOG" target="_blank" rel="noopener noreferrer">indigo</a></span></div></div></footer></div></main><button type="button" class="v-btn v-btn--bottom v-btn--floating v-btn--fixed v-btn--right accent" style="display:none;"><div class="v-btn__content"><i class="fa fa-lg fa-chevron-up"></i></div></button></div></div></div>
    <script src="/BLOG/assets/js/1.39b9c99c.js" defer></script><script src="/BLOG/assets/js/app.1a725be8.js" defer></script>
  </body>
</html>
